/*
 * ReferenceMap.java
 *
 * Created on December 11, 2006, 9:58 PM
 *
 * To change this template, choose Tools | Template Manager
 * and open the template in the editor.
 */

package org.jboss.el.util;

import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.SoftReference;
import java.lang.ref.WeakReference;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;

/**
 *
 * @author jhook
 */
public abstract class ReferenceCache<K,V> {

    public abstract class ReferenceFactory<K,V> {
        public abstract ReferenceKey<K> createKey(ReferenceQueue queue, K key);
        public abstract ReferenceValue<V> createValue(ReferenceQueue queue, V value);
    }
    
    private class StrongReferenceFactory extends ReferenceFactory<K,V> {
        public ReferenceValue<V> createValue(ReferenceQueue queue, final V value) {
            return new ReferenceValue<V>() {
                public V get() {
                    return value;
                }
            };
        }
        
        public ReferenceKey<K> createKey(ReferenceQueue queue, final K key) {
            return new ReferenceKey<K>(key) {
                public K get() {
                    return key;
                }
            };
        }
    }
    
    private class WeakReferenceFactory extends ReferenceFactory<K,V> {
        private class WeakReferenceKey extends ReferenceKey<K> {
            private final WeakReference<K> ref;
            
            public WeakReferenceKey(final ReferenceQueue queue, final K key) {
                super(key);
                this.ref = new WeakReference<K>(key, queue) {
                    public void clear() {
                        remove();
                        super.clear();
                    }
                };
            }
            
            public K get() {
                return this.ref.get();
            }
        }
        
        public ReferenceValue<V> createValue(final ReferenceQueue queue, final V value) {
            return new ReferenceValue<V>() {
                private final WeakReference<V> ref = new WeakReference<V>(value, queue);
                public V get() {
                    return ref.get();
                }
            };
        }
        
        public ReferenceKey<K> createKey(ReferenceQueue queue, K key) {
            return new WeakReferenceKey(queue, key);
        }
    }
    
    private class SoftReferenceFactory extends ReferenceFactory<K,V> {
        private class SoftReferenceKey extends ReferenceKey<K> {
            private final SoftReference<K> ref;
            
            public SoftReferenceKey(final ReferenceQueue queue, final K key) {
                super(key);
                this.ref = new SoftReference<K>(key, queue) {
                    public void clear() {
                        remove();
                        super.clear();
                    }
                };
            }
            
            public K get() {
                return this.ref.get();
            }
        }
        
        public ReferenceValue<V> createValue(final ReferenceQueue queue, final V value) {
            return new ReferenceValue<V>() {
                private final SoftReference<V> ref = new SoftReference<V>(value, queue);
                public V get() {
                    return ref.get();
                }
            };
        }
        
        public ReferenceKey<K> createKey(final ReferenceQueue queue, final K key) {
            return new SoftReferenceKey(queue, key);
        }
    }
    
    public abstract class ReferenceKey<K> {
        private final int hashCode;
        
        public ReferenceKey(K key) {
            this.hashCode = key.hashCode();
        }
        
        protected abstract K get();
        
        public boolean equals(Object obj) {
            if (this == obj) return true;
            K me = this.get();
            if (me != null) {
                if (obj == me) return true;
                if (obj instanceof ReferenceKey) {
                    K them = ((ReferenceKey<K>) obj).get();
                    return me == them || me.equals(them);
                }
            }
            return false;
        }
        
        public void remove() {
            cache.remove(this);
        }
        
        public int hashCode() {
            return this.hashCode;
        }
    }
    
    public interface ReferenceValue<V> {
        public V get();
    }

    private class ReferenceQueueRunner 
        extends ReferenceQueue 
        implements Runnable
    {
        public void run() {            
            while (true) {
                try {
                    Reference ref = this.remove();
                    if (ref != null) {
                        ref.clear();
                    }
                } catch (InterruptedException e) {
                    break;
                    //e.printStackTrace();
                }
            }
        }
    }

    private final ConcurrentMap<ReferenceKey<K>,ReferenceValue<V>> cache;
    private final ReferenceFactory keyFactory;
    private final ReferenceFactory valueFactory;
    private final ReferenceFactory lookupFactory;
    private final ReferenceQueueRunner queue;
    private Thread queueMonitor;
    
    public static enum Type { Strong, Weak, Soft };
    
    /**
     * Creates a new instance of ReferenceMap
     */
    public ReferenceCache(Type keyType, Type valueType) {
        this(keyType, valueType, 0);
    }
    
    public ReferenceCache(Type keyType, Type valueType, int initialSize) {
        this.keyFactory = toFactory(keyType);
        this.valueFactory = toFactory(valueType);
        this.lookupFactory = new StrongReferenceFactory();
        this.cache = new ConcurrentHashMap<ReferenceKey<K>, ReferenceValue<V>>(initialSize);
        this.queue = new ReferenceQueueRunner();
    }
    
    
    public void startMonitor() {
        if (queueMonitor == null) {
            queueMonitor = new Thread(this.queue);
            queueMonitor.setName("jboss EL reference queue cleanup thread");
            queueMonitor.setDaemon(true);        
            queueMonitor.start();
        }
    }
    
    public void stopMonitor() {
        if (queueMonitor!=null) {
            queueMonitor.interrupt();
            queueMonitor = null;
        }
    }
    
    private final ReferenceFactory<K,V> toFactory(Type type) {
        switch (type) {
            case Strong : return new StrongReferenceFactory();
            case Weak : return new WeakReferenceFactory();
            case Soft : return new SoftReferenceFactory();
            default : throw new IllegalArgumentException("Invalid ReferenceType: " + type);
        }
    }
    
    protected abstract V create(K key);
    
    public V get(final Object key) {
        try {
            ReferenceKey<K> refKey = this.lookupFactory.createKey(this.queue, (K) key);
            ReferenceValue<V> refVal = this.cache.get(refKey);
            V value = dereferenceValue(refVal);
            if (value != null) {
                return value;
            } else {
                V created = create((K) key);
                refVal = valueFactory.createValue(queue, created);
                refKey = this.keyFactory.createKey(this.queue, (K) key);
                refVal = this.cache.putIfAbsent(refKey, refVal);
                value = dereferenceValue(refVal);
                if (value == null) {
                    value = this.create((K) key);
                    this.put((K) key, value);
                }
                return value;
            }
        } catch (Exception e) {
            if (e instanceof RuntimeException) throw (RuntimeException) e;
            throw new IllegalStateException(e);
        }
    }
    
    private V dereferenceValue(ReferenceValue<V> refValue) {
        return refValue == null ? null : refValue.get();
    }
    
    public V put(K key, final V value) {
        ReferenceKey refKey = this.keyFactory.createKey(this.queue, key);
        ReferenceValue<V> refVal = valueFactory.createValue(queue, value);
        refVal = this.cache.putIfAbsent(refKey, refVal);
        return value;
    }
    
    public V remove(Object key) {
        ReferenceKey<K> keyRef = this.lookupFactory.createKey(this.queue, key);
        return this.dereferenceValue(this.cache.remove(keyRef));
    }
    
    public int size() {
        return this.cache.size();
    }
    
    public void clear() {
        this.cache.clear();
    }
}